--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit cee526b3a8ab411a20ba554a5447052c57515eef
Parents : 9566e49
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-16T10:47:52-05:00
feat: update network visualiser with WebGL rendering options and user preferences
Changes
24 files changed, 308 insertions(+), 38 deletions(-)
Diff
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dd1c1c53..1ec63f2e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,6 +9,7 @@
# actions/setup-python@v6.2.0 a309ff8b426b58ec0e2a45f0f869d46889d02405
# actions/upload-artifact@v5.0.0 330a01c490aca151604b8cf639adc76d48f6c5d4
# actions/download-artifact@v5.0.0 634f93cb2916e3fdff6788551b99b062d0335ce0
+# actions/setup-go@v7.0.0 b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
name: CI
@@ -36,6 +37,7 @@ env:
NODE_VERSION: "24"
UV_VERSION: "0.11.15"
PNPM_VERSION: "11.1.2"
+ GO_VERSION: "1.24.x"
RNS_REQUIRED_SIGNER: e46112d44649266d71fe2193e00a4710
jobs:
@@ -102,6 +104,44 @@ jobs:
- name: Verify workspace clean
uses: ./.github/actions/verify-workspace-clean
+ visualiser-wasm:
+ name: Visualiser WASM (Go)
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: read
+ steps:
+ - name: Checkout
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
+
+ - name: Set up Go
+ uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
+ with:
+ go-version: ${{ env.GO_VERSION }}
+
+ - name: Setup Task
+ run: sh scripts/ci/setup-task.sh 3.49.1
+
+ - name: Run visualiser-wasm tests
+ run: task test:visualiser-wasm
+
+ - name: Build visualiser-wasm artifacts
+ run: task build:visualiser-wasm
+
+ - name: Verify integrity.json stamped
+ run: |
+ set -euo pipefail
+ test -f meshchatx/src/frontend/public/vendor/visualiser-wasm/integrity.json
+ python3 - <<'PY'
+ import json
+ from pathlib import Path
+ data = json.loads(Path("meshchatx/src/frontend/public/vendor/visualiser-wasm/integrity.json").read_text())
+ assert data.get("version") == "1.2.0", data
+ assert str(data.get("wasm", "")).startswith("sha384-"), data
+ assert str(data.get("wasmExec", "")).startswith("sha384-"), data
+ print("visualiser-wasm integrity OK", data["version"])
+ PY
+
backend-tests:
name: Backend tests (Python 3.14) - Shard ${{ matrix.shard }} / 4
runs-on: ubuntu-latest
diff --git a/.github/workflows/frontend-build.yml b/.github/workflows/frontend-build.yml
index 96fa87e7..26a9c01f 100644
--- a/.github/workflows/frontend-build.yml
+++ b/.github/workflows/frontend-build.yml
@@ -8,6 +8,7 @@
# actions/setup-python@v6.2.0 a309ff8b426b58ec0e2a45f0f869d46889d02405
# actions/setup-node@v6.1.0 395ad3262231945c25e8478fd5baf05154b1d79f
# actions/upload-artifact@v5.0.0 330a01c490aca151604b8cf639adc76d48f6c5d4
+# actions/setup-go@v7.0.0 b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
name: Frontend build
@@ -90,6 +91,11 @@ jobs:
node-version: ${{ inputs.node_version }}
pnpm-version: ${{ inputs.pnpm_version }}
+ - name: Set up Go (visualiser-wasm)
+ uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
+ with:
+ go-version: "1.24.x"
+
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 28aa0400..19408fce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file.
- RSM signing and verification for meshchatx.rsm (CI and pre-commit resign)
- Notification sound settings
- LXMFy 1.6.5 vendor refresh, wasmtime, mutation test tasks
+- Network visualiser WebGL + WASM renderer (vis-network fallback) and Settings renderer preference
### Changed
diff --git a/meshchatx.rsm b/meshchatx.rsm
index e53c2997..88b20e53 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ
diff --git a/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue b/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue
index 156232c8..384e420e 100644
--- a/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue
+++ b/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue
@@ -95,6 +95,7 @@ import {
persistVisualiserLiveLayout,
VISUALISER_DISPLAY_PREFS_CHANGED,
} from "../../js/settings/settingsVisualiserPrefs.js";
+import ToastUtils from "../../js/ToastUtils";
const HOP_MAX_FILTER_STORAGE_KEY = "meshchatx.visualiser.maxHops";
@@ -186,6 +187,7 @@ export default {
isUpdating: false,
isLoading: false,
enablePhysics: displayPrefs.enablePhysics,
+ preferredRenderer: displayPrefs.renderer || "auto",
showDisabledInterfaces: displayPrefs.showDisabledInterfaces,
showDiscoveredInterfaces: displayPrefs.showDiscoveredInterfaces,
loadingStatus: "Initializing...",
@@ -366,9 +368,14 @@ export default {
this.isShowingControls = false;
}
- this._visualiserPrefsHandler = () => {
+ this._visualiserPrefsHandler = async () => {
+ const prevRenderer = this.preferredRenderer;
this.loadVisualiserDisplayPrefs();
this.applyBatterySaverVisualiserPrefs();
+ if (this.preferredRenderer !== prevRenderer) {
+ await this.reinitRenderer();
+ return;
+ }
if (this.hasRenderer) {
this.processVisualization();
}
@@ -805,6 +812,54 @@ export default {
this.showDiscoveredInterfaces = p.showDiscoveredInterfaces;
this.enablePhysics = p.enablePhysics;
this.autoReload = p.autoReload;
+ this.preferredRenderer = p.renderer || "auto";
+ },
+ destroyActiveRenderer() {
+ this.hoverTooltip = null;
+ if (this.webglEngine) {
+ this.webglEngine.destroy();
+ this.webglEngine = null;
+ }
+ if (this.network) {
+ this.network.destroy();
+ this.network = null;
+ }
+ try {
+ this.nodes.clear();
+ this.edges.clear();
+ } catch {
+ /* ignore */
+ }
+ this.rendererMode = "vis";
+ },
+ async reinitRenderer() {
+ this.destroyActiveRenderer();
+ await this.init({ skipWarm: true });
+ },
+ async tryStartWebGL() {
+ const canvas = this.$refs.webglCanvas;
+ if (!canvas || !canUseVisualiserWebGL()) {
+ return false;
+ }
+ try {
+ this.webglEngine = createVisualiserWebGLEngine(canvas, {
+ getLiveLayout: () => this.enablePhysics === true,
+ isDark: () => document.documentElement.classList.contains("dark"),
+ onNodeActivate: (id, meta) => this.onWebGLNodeActivate(id, meta),
+ onHover: (id, meta, x, y) => this.onWebGLHover(id, meta, x, y),
+ });
+ this.rendererMode = "webgl";
+ this.engineMode = "webgl";
+ return true;
+ } catch (e) {
+ console.warn("WebGL visualiser failed:", e);
+ if (this.webglEngine) {
+ this.webglEngine.destroy();
+ this.webglEngine = null;
+ }
+ this.rendererMode = "vis";
+ return false;
+ }
},
refreshPhysicsEnabled() {
if (this.webglEngine) {
@@ -892,30 +947,26 @@ export default {
posById[id] = { x: v.x, y: v.y };
return v;
},
- async init() {
- await warmVisualiserWasm();
- const canvas = this.$refs.webglCanvas;
- if (canvas && canUseVisualiserWebGL(canvas)) {
- try {
- this.webglEngine = createVisualiserWebGLEngine(canvas, {
- getLiveLayout: () => this.enablePhysics === true,
- isDark: () => document.documentElement.classList.contains("dark"),
- onNodeActivate: (id, meta) => this.onWebGLNodeActivate(id, meta),
- onHover: (id, meta, x, y) => this.onWebGLHover(id, meta, x, y),
- });
- this.rendererMode = "webgl";
- this.engineMode = "webgl";
- await this.manualUpdate();
- this.restartAutoReloadInterval();
- return;
- } catch (e) {
- console.warn("WebGL visualiser failed, falling back to vis-network:", e);
- if (this.webglEngine) {
- this.webglEngine.destroy();
- this.webglEngine = null;
- }
- this.rendererMode = "vis";
- }
+ async init(opts = {}) {
+ if (!opts.skipWarm) {
+ await warmVisualiserWasm();
+ }
+ const preferred = this.preferredRenderer || loadVisualiserDisplayPrefs().renderer || "auto";
+
+ if (preferred === "vis") {
+ await this.initVisNetwork();
+ return;
+ }
+
+ const started = await this.tryStartWebGL();
+ if (started) {
+ await this.manualUpdate();
+ this.restartAutoReloadInterval();
+ return;
+ }
+
+ if (preferred === "webgl") {
+ ToastUtils.warning(this.$t("visualiser.renderer_webgl_unavailable"));
}
await this.initVisNetwork();
},
diff --git a/meshchatx/src/frontend/components/settings/SettingsPage.vue b/meshchatx/src/frontend/components/settings/SettingsPage.vue
index fae8ab63..21e8a1aa 100644
--- a/meshchatx/src/frontend/components/settings/SettingsPage.vue
+++ b/meshchatx/src/frontend/components/settings/SettingsPage.vue
@@ -1896,6 +1896,24 @@
</div>
</header>
<div class="settings-section__body space-y-4">
+ <div class="space-y-2">
+ <div class="text-sm font-medium text-gray-900 dark:text-gray-100">
+ {{ $t("visualiser.renderer_title") }}
+ </div>
+ <p class="text-xs text-gray-600 dark:text-gray-400">
+ {{ $t("visualiser.renderer_desc") }}
+ </p>
+ <select
+ id="settings-visualiser-renderer"
+ v-model="visualiserRenderer"
+ class="input-field"
+ @change="onVisualiserRendererChange"
+ >
+ <option value="auto">{{ $t("visualiser.renderer_option_auto") }}</option>
+ <option value="webgl">{{ $t("visualiser.renderer_option_webgl") }}</option>
+ <option value="vis">{{ $t("visualiser.renderer_option_vis") }}</option>
+ </select>
+ </div>
<label class="setting-toggle">
<Toggle
id="settings-visualiser-offline"
@@ -3581,6 +3599,7 @@ import {
loadVisualiserDisplayPrefs,
persistVisualiserShowDisabled,
persistVisualiserShowDiscovered,
+ persistVisualiserRenderer,
} from "../../js/settings/settingsVisualiserPrefs";
import { loadBatterySaverPrefs, saveBatterySaverPrefs } from "../../js/settings/batterySaverPrefs.js";
import {
@@ -3752,6 +3771,7 @@ export default {
gifImportReplaceDuplicates: false,
visualiserShowDisabledInterfaces: false,
visualiserShowDiscoveredInterfaces: false,
+ visualiserRenderer: "auto",
batterySaver: loadBatterySaverPrefs(),
batteryInterfaceRows: [],
batteryBitrateBusy: false,
@@ -4307,6 +4327,7 @@ export default {
const p = loadVisualiserDisplayPrefs();
this.visualiserShowDisabledInterfaces = p.showDisabledInterfaces;
this.visualiserShowDiscoveredInterfaces = p.showDiscoveredInterfaces;
+ this.visualiserRenderer = p.renderer || "auto";
},
onVisualiserShowDisabledChange(val) {
this.visualiserShowDisabledInterfaces = val;
@@ -4316,6 +4337,9 @@ export default {
this.visualiserShowDiscoveredInterfaces = val;
persistVisualiserShowDiscovered(val);
},
+ onVisualiserRendererChange() {
+ persistVisualiserRenderer(this.visualiserRenderer);
+ },
async getTrustedTelemetryPeers() {
try {
const response = await window.api.get("/api/v1/telemetry/trusted-peers");
diff --git a/meshchatx/src/frontend/js/settings/settingsVisualiserPrefs.js b/meshchatx/src/frontend/js/settings/settingsVisualiserPrefs.js
index 66f34814..ed09d002 100644
--- a/meshchatx/src/frontend/js/settings/settingsVisualiserPrefs.js
+++ b/meshchatx/src/frontend/js/settings/settingsVisualiserPrefs.js
@@ -6,9 +6,14 @@ const KEY_DISABLED = "meshchatx.visualiser.showDisabledInterfaces";
const KEY_DISCOVERED = "meshchatx.visualiser.showDiscoveredInterfaces";
const KEY_LIVE_LAYOUT = "meshchatx.visualiser.enablePhysics";
const KEY_AUTO_RELOAD = "meshchatx.visualiser.autoReload";
+const KEY_RENDERER = "meshchatx.visualiser.renderer";
export const VISUALISER_DISPLAY_PREFS_CHANGED = "visualiser-display-prefs-changed";
+/** @typedef {"auto" | "webgl" | "vis"} VisualiserRendererPref */
+
+export const VISUALISER_RENDERER_OPTIONS = ["auto", "webgl", "vis"];
+
/**
* @param {string} key
* @param {boolean} defaultValue
@@ -42,12 +47,38 @@ function writeBool(key, val) {
}
}
+/**
+ * @param {unknown} raw
+ * @returns {VisualiserRendererPref}
+ */
+export function normalizeVisualiserRenderer(raw) {
+ if (raw === "webgl" || raw === "vis" || raw === "auto") {
+ return raw;
+ }
+ return "auto";
+}
+
+/**
+ * @returns {VisualiserRendererPref}
+ */
+function readRenderer() {
+ try {
+ if (typeof localStorage === "undefined") {
+ return "auto";
+ }
+ return normalizeVisualiserRenderer(localStorage.getItem(KEY_RENDERER));
+ } catch {
+ return "auto";
+ }
+}
+
/**
* @returns {{
* showDisabledInterfaces: boolean,
* showDiscoveredInterfaces: boolean,
* enablePhysics: boolean,
* autoReload: boolean,
+ * renderer: VisualiserRendererPref,
* }}
*/
export function loadVisualiserDisplayPrefs() {
@@ -57,6 +88,7 @@ export function loadVisualiserDisplayPrefs() {
// Live Layout defaults on when never set.
enablePhysics: readBool(KEY_LIVE_LAYOUT, true),
autoReload: readBool(KEY_AUTO_RELOAD, false),
+ renderer: readRenderer(),
};
}
@@ -97,3 +129,21 @@ export function persistVisualiserAutoReload(val, opts = {}) {
GlobalEmitter.emit(VISUALISER_DISPLAY_PREFS_CHANGED);
}
}
+
+/**
+ * @param {unknown} val
+ * @param {{ emit?: boolean }} [opts]
+ */
+export function persistVisualiserRenderer(val, opts = {}) {
+ const next = normalizeVisualiserRenderer(val);
+ try {
+ if (typeof localStorage !== "undefined") {
+ localStorage.setItem(KEY_RENDERER, next);
+ }
+ } catch {
+ /* ignore */
+ }
+ if (opts.emit !== false) {
+ GlobalEmitter.emit(VISUALISER_DISPLAY_PREFS_CHANGED);
+ }
+}
diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index 5f0995ad..27bb2b09 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -3367,7 +3367,13 @@
"search_nodes_placeholder": "Knoten suchen ({count})...",
"clear_search": "Suche leeren",
"engine_webgl": "WebGL",
- "engine_webgl_hint": "Graph, Layout und Zeichenpuffer laufen in Go-WebAssembly. Pixelzeichnung mit WebGL2"
+ "engine_webgl_hint": "Graph, Layout und Zeichenpuffer laufen in Go-WebAssembly. Pixelzeichnung mit WebGL2",
+ "renderer_title": "Renderer",
+ "renderer_desc": "Auto bevorzugt WebGL mit WASM und fällt sonst auf vis-network zurück. Erzwingen Sie einen Pfad zum Debuggen oder für ältere GPUs.",
+ "renderer_option_auto": "Auto (WebGL wenn verfügbar)",
+ "renderer_option_webgl": "WebGL + WASM",
+ "renderer_option_vis": "vis-network (Canvas)",
+ "renderer_webgl_unavailable": "WebGL-Renderer nicht verfügbar. Nutze vis-network-Fallback."
},
"banishment": {
"title": "Verbannt",
diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index e64bc3ea..766b34f4 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -1895,6 +1895,12 @@
"visualiser": {
"title": "Network Visualiser",
"description": "Graph of discovered nodes and links.",
+ "renderer_title": "Renderer",
+ "renderer_desc": "Auto prefers WebGL with WASM when available, then falls back to vis-network. Force a path for debugging or older GPUs.",
+ "renderer_option_auto": "Auto (WebGL when available)",
+ "renderer_option_webgl": "WebGL + WASM",
+ "renderer_option_vis": "vis-network (canvas)",
+ "renderer_webgl_unavailable": "WebGL renderer unavailable. Using vis-network fallback.",
"reticulum_mesh": "Reticulum Mesh",
"network_visualizer": "Network Visualizer",
"node_info": "Node Info",
diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index d463ee63..20b49464 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -1963,7 +1963,13 @@
"search_nodes_placeholder": "Buscar nodos ({count})...",
"clear_search": "Borrar busqueda",
"engine_webgl": "WebGL",
- "engine_webgl_hint": "Grafo, layout y buffers de dibujo en Go WebAssembly. Pixels con WebGL2"
+ "engine_webgl_hint": "Grafo, layout y buffers de dibujo en Go WebAssembly. Pixels con WebGL2",
+ "renderer_title": "Renderizador",
+ "renderer_desc": "Auto prefiere WebGL con WASM cuando está disponible y luego usa vis-network. Fuerza una ruta para depurar o GPUs antiguas.",
+ "renderer_option_auto": "Auto (WebGL si está disponible)",
+ "renderer_option_webgl": "WebGL + WASM",
+ "renderer_option_vis": "vis-network (canvas)",
+ "renderer_webgl_unavailable": "Renderizador WebGL no disponible. Usando reserva vis-network."
},
"banishment": {
"title": "Desterrados",
diff --git a/meshchatx/src/frontend/locales/fi.json b/meshchatx/src/frontend/locales/fi.json
index 73123347..005b29d4 100644
--- a/meshchatx/src/frontend/locales/fi.json
+++ b/meshchatx/src/frontend/locales/fi.json
@@ -1963,7 +1963,13 @@
"search_nodes_placeholder": "Hae solmuja ({count})...",
"clear_search": "Tyhjennä haku",
"engine_webgl": "WebGL",
- "engine_webgl_hint": "Kaavio, asettelu ja piirtopuskurit Go WebAssemblyssa. Pikselit WebGL2:lla"
+ "engine_webgl_hint": "Kaavio, asettelu ja piirtopuskurit Go WebAssemblyssa. Pikselit WebGL2:lla",
+ "renderer_title": "Renderöijä",
+ "renderer_desc": "Auto suosii WebGL+WASM:ia kun saatavilla ja palaa sitten vis-networkiin. Pakota polku debuggaukseen tai vanhoille GPU:ille.",
+ "renderer_option_auto": "Auto (WebGL jos saatavilla)",
+ "renderer_option_webgl": "WebGL + WASM",
+ "renderer_option_vis": "vis-network (canvas)",
+ "renderer_webgl_unavailable": "WebGL-renderöijä ei ole saatavilla. Käytetään vis-network-varausta."
},
"banishment": {
"title": "Karkotus",
diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index a60fe661..45a9b15e 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -1963,7 +1963,13 @@
"search_nodes_placeholder": "Rechercher des noeuds ({count})...",
"clear_search": "Effacer la recherche",
"engine_webgl": "WebGL",
- "engine_webgl_hint": "Graphe, layout et buffers de dessin en Go WebAssembly. Pixels via WebGL2"
+ "engine_webgl_hint": "Graphe, layout et buffers de dessin en Go WebAssembly. Pixels via WebGL2",
+ "renderer_title": "Moteur de rendu",
+ "renderer_desc": "Auto préfère WebGL avec WASM puis revient à vis-network. Forcez un chemin pour le débogage ou les anciens GPU.",
+ "renderer_option_auto": "Auto (WebGL si disponible)",
+ "renderer_option_webgl": "WebGL + WASM",
+ "renderer_option_vis": "vis-network (canvas)",
+ "renderer_webgl_unavailable": "Rendu WebGL indisponible. Repli sur vis-network."
},
"banishment": {
"title": "Interdit",
diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index 35e0181a..a82f57b4 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -2015,7 +2015,13 @@
"search_nodes_placeholder": "Cerca nodi ({count})...",
"clear_search": "Cancella ricerca",
"engine_webgl": "WebGL",
- "engine_webgl_hint": "Grafo, layout e buffer di disegno in Go WebAssembly. Pixel con WebGL2"
+ "engine_webgl_hint": "Grafo, layout e buffer di disegno in Go WebAssembly. Pixel con WebGL2",
+ "renderer_title": "Renderer",
+ "renderer_desc": "Auto preferisce WebGL con WASM quando disponibile, poi passa a vis-network. Forza un percorso per debug o GPU datate.",
+ "renderer_option_auto": "Auto (WebGL se disponibile)",
+ "renderer_option_webgl": "WebGL + WASM",
+ "renderer_option_vis": "vis-network (canvas)",
+ "renderer_webgl_unavailable": "Renderer WebGL non disponibile. Uso fallback vis-network."
},
"banishment": {
"title": "Esiliati",
diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index 2a38f384..23883d28 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -1963,7 +1963,13 @@
"search_nodes_placeholder": "Zoek knooppunten ({count})...",
"clear_search": "Zoekopdracht wissen",
"engine_webgl": "WebGL",
- "engine_webgl_hint": "Grafiek, layout en tekenbuffers in Go WebAssembly. Pixels met WebGL2"
+ "engine_webgl_hint": "Grafiek, layout en tekenbuffers in Go WebAssembly. Pixels met WebGL2",
+ "renderer_title": "Renderer",
+ "renderer_desc": "Auto prefereert WebGL met WASM indien beschikbaar, anders vis-network. Forceer een pad voor debug of oudere GPU's.",
+ "renderer_option_auto": "Auto (WebGL indien beschikbaar)",
+ "renderer_option_webgl": "WebGL + WASM",
+ "renderer_option_vis": "vis-network (canvas)",
+ "renderer_webgl_unavailable": "WebGL-renderer niet beschikbaar. Gebruik vis-network-fallback."
},
"banishment": {
"title": "Verbannen",
diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index 01da736c..6e6635d6 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -3367,7 +3367,13 @@
"search_nodes_placeholder": "Поиск узлов ({count})...",
"clear_search": "Очистить поиск",
"engine_webgl": "WebGL",
- "engine_webgl_hint": "Граф, раскладка и буферы отрисовки в Go WebAssembly. Пиксели через WebGL2"
+ "engine_webgl_hint": "Граф, раскладка и буферы отрисовки в Go WebAssembly. Пиксели через WebGL2",
+ "renderer_title": "Рендерер",
+ "renderer_desc": "Auto предпочитает WebGL с WASM при наличии, иначе vis-network. Принудительный выбор для отладки или старых GPU.",
+ "renderer_option_auto": "Auto (WebGL если доступен)",
+ "renderer_option_webgl": "WebGL + WASM",
+ "renderer_option_vis": "vis-network (canvas)",
+ "renderer_webgl_unavailable": "WebGL-рендерер недоступен. Используется запасной vis-network."
},
"banishment": {
"title": "Забаненные",
diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index 4b29672e..4cb54374 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -1963,7 +1963,13 @@
"search_nodes_placeholder": "搜索节点 ({count})...",
"clear_search": "清除搜索",
"engine_webgl": "WebGL",
- "engine_webgl_hint": "图构建、布局与绘制缓冲在 Go WebAssembly 中运行。像素由 WebGL2 绘制"
+ "engine_webgl_hint": "图构建、布局与绘制缓冲在 Go WebAssembly 中运行。像素由 WebGL2 绘制",
+ "renderer_title": "渲染器",
+ "renderer_desc": "自动在可用时优先使用 WebGL+WASM,否则回退到 vis-network。可强制选择以便调试或兼容旧 GPU。",
+ "renderer_option_auto": "自动(可用时使用 WebGL)",
+ "renderer_option_webgl": "WebGL + WASM",
+ "renderer_option_vis": "vis-network(canvas)",
+ "renderer_webgl_unavailable": "WebGL 渲染器不可用。已回退到 vis-network。"
},
"banishment": {
"title": "放逐",
diff --git a/meshchatx/src/frontend/public/vendor/visualiser-wasm/integrity.json b/meshchatx/src/frontend/public/vendor/visualiser-wasm/integrity.json
index 0d0d63ad..2202f63e 100644
--- a/meshchatx/src/frontend/public/vendor/visualiser-wasm/integrity.json
+++ b/meshchatx/src/frontend/public/vendor/visualiser-wasm/integrity.json
@@ -1,6 +1,6 @@
{
- "version": "1.0.0",
- "wasm": "sha384-ERsG9mb3L8kjxDEvn36T/mdj3Cz4zPA7U/hNo6Nppt9sjE0VAuNT3XjHsFDx9P/b",
+ "version": "1.2.0",
+ "wasm": "sha384-kx5+sGvflCkoO65dt5IcMJVB/8y85IAwT6zUbAKjha1XK/8p3tMp+QFwX5hd2DYN",
"wasmExec": "sha384-PWCs+V4BDf9yY1yjkD/p+9xNEs4iEbuvq+HezAOJiY3XL5GI6VyJXMsvnjiwNbce",
"wasmExecSource": "/usr/lib/go/lib/wasm/wasm_exec.js"
}
diff --git a/scripts/build-visualiser-wasm.mjs b/scripts/build-visualiser-wasm.mjs
index 78818976..05781d3c 100644
--- a/scripts/build-visualiser-wasm.mjs
+++ b/scripts/build-visualiser-wasm.mjs
@@ -16,7 +16,7 @@ const GO_MOD_DIR = path.join(REPO_ROOT, "visualiser-wasm");
const OUT_DIR = path.join(REPO_ROOT, "meshchatx", "src", "frontend", "public", "vendor", "visualiser-wasm");
const WASM_NAME = "visualiser.wasm";
const EXEC_NAME = "wasm_exec.js";
-const VERSION = "1.0.0";
+const VERSION = "1.2.0";
function computeSri(buf) {
return `sha384-${crypto.createHash("sha384").update(buf).digest("base64")}`;
diff --git a/tests/backend/fixtures/http_api_routes.json b/tests/backend/fixtures/http_api_routes.json
index 6d686ebd..76f36d9d 100644
--- a/tests/backend/fixtures/http_api_routes.json
+++ b/tests/backend/fixtures/http_api_routes.json
@@ -908,6 +908,10 @@
"method": "POST",
"path": "/api/v1/reticulum/interfaces/add"
},
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interfaces/bitrates"
+ },
{
"method": "POST",
"path": "/api/v1/reticulum/interfaces/delete"
diff --git a/tests/e2e/conversations-nomad-visualiser.spec.js b/tests/e2e/conversations-nomad-visualiser.spec.js
index 1f1f6e32..562bc4b3 100644
--- a/tests/e2e/conversations-nomad-visualiser.spec.js
+++ b/tests/e2e/conversations-nomad-visualiser.spec.js
@@ -60,12 +60,14 @@ test.describe("Network visualiser", () => {
await prepareE2eSession(request);
});
- test("shows mesh header, graph container, and node search", async ({ page }) => {
+ test("shows mesh header, graph hosts, and node search", async ({ page }) => {
await page.goto("/#/network-visualiser");
await expect(page).toHaveURL(/#\/network-visualiser/);
await expect(page.getByText("Reticulum Mesh", { exact: true })).toBeVisible({ timeout: 30000 });
await expect(page.locator("#network")).toBeAttached();
+ await expect(page.locator("#network-webgl")).toBeAttached();
await expect(page.getByPlaceholder(/Search nodes \(\d+\)/)).toBeVisible({ timeout: 30000 });
+ await expect(page.getByText(/WebGL|WASM|JS fallback|Checking/)).toBeVisible({ timeout: 30000 });
});
test("collapses and expands control panel via header", async ({ page }) => {
diff --git a/tests/frontend/NetworkVisualiser.test.js b/tests/frontend/NetworkVisualiser.test.js
index 4dc75612..cceb9dd0 100644
--- a/tests/frontend/NetworkVisualiser.test.js
+++ b/tests/frontend/NetworkVisualiser.test.js
@@ -160,6 +160,7 @@ describe("NetworkVisualiser.vue", () => {
"visualiser.nodes": "Nodes",
"visualiser.links": "Links",
"visualiser.engine_wasm": "WASM",
+ "visualiser.engine_webgl": "WebGL",
"visualiser.engine_fallback": "JS fallback",
"visualiser.engine_checking": "Checking",
};
diff --git a/tests/frontend/SettingsPage.config-persistence.test.js b/tests/frontend/SettingsPage.config-persistence.test.js
index 0f2da83d..b14d6a84 100644
--- a/tests/frontend/SettingsPage.config-persistence.test.js
+++ b/tests/frontend/SettingsPage.config-persistence.test.js
@@ -542,6 +542,14 @@ describe("SettingsPage — visualiser display prefs (localStorage + emitter)", (
expect(GlobalEmitter.emit).toHaveBeenCalledWith("visualiser-display-prefs-changed");
});
+ it("onVisualiserRendererChange persists renderer and emits", async () => {
+ const w = await mountSettingsPage(api);
+ w.vm.visualiserRenderer = "webgl";
+ w.vm.onVisualiserRendererChange();
+ expect(localStorage.getItem("meshchatx.visualiser.renderer")).toBe("webgl");
+ expect(GlobalEmitter.emit).toHaveBeenCalledWith("visualiser-display-prefs-changed");
+ });
+
it("onDetailedOutboundSendStatusChange updates GlobalState and localStorage", async () => {
localStorage.removeItem("meshchatx_detailed_outbound_send_status");
const w = await mountSettingsPage(api);
diff --git a/tests/frontend/behaviorContracts.test.js b/tests/frontend/behaviorContracts.test.js
index 4d24ecf0..bf087e94 100644
--- a/tests/frontend/behaviorContracts.test.js
+++ b/tests/frontend/behaviorContracts.test.js
@@ -260,4 +260,21 @@ describe("behavior contracts: network visualiser performance", () => {
expect(perf).toContain("dedupeIconQueueEntries");
expect(perf).toContain("pickAdaptiveFetchConcurrency");
});
+
+ it("prefers WebGL with WASM scene and keeps vis-network fallback", () => {
+ const src = readSource("meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue");
+ expect(src).toContain("tryStartWebGL");
+ expect(src).toContain("initVisNetwork");
+ expect(src).toContain("preferredRenderer");
+ expect(src).toContain('name: "nomadnetwork"');
+ expect(src).toContain("openAnnounceDestination");
+ const engine = readSource("meshchatx/src/frontend/js/networkVisualiserWebGLEngine.js");
+ expect(engine).toContain("meshchatxVisualiserSceneSet");
+ expect(engine).toContain("createVisualiserWebGLEngine");
+ const prefs = readSource("meshchatx/src/frontend/js/settings/settingsVisualiserPrefs.js");
+ expect(prefs).toContain("persistVisualiserRenderer");
+ expect(prefs).toContain('"auto"');
+ expect(prefs).toContain('"webgl"');
+ expect(prefs).toContain('"vis"');
+ });
});
diff --git a/tests/frontend/settingsVisualiserPrefs.test.js b/tests/frontend/settingsVisualiserPrefs.test.js
index 48ec8bbe..cebcdaa9 100644
--- a/tests/frontend/settingsVisualiserPrefs.test.js
+++ b/tests/frontend/settingsVisualiserPrefs.test.js
@@ -5,6 +5,8 @@ import {
persistVisualiserAutoReload,
persistVisualiserShowDisabled,
persistVisualiserShowDiscovered,
+ persistVisualiserRenderer,
+ normalizeVisualiserRenderer,
} from "@/js/settings/settingsVisualiserPrefs.js";
describe("settingsVisualiserPrefs", () => {
@@ -12,12 +14,13 @@ describe("settingsVisualiserPrefs", () => {
localStorage.clear();
});
- it("defaults live layout on and auto-reload off", () => {
+ it("defaults live layout on, auto-reload off, renderer auto", () => {
expect(loadVisualiserDisplayPrefs()).toEqual({
showDisabledInterfaces: false,
showDiscoveredInterfaces: false,
enablePhysics: true,
autoReload: false,
+ renderer: "auto",
});
});
@@ -31,6 +34,7 @@ describe("settingsVisualiserPrefs", () => {
showDiscoveredInterfaces: true,
enablePhysics: false,
autoReload: true,
+ renderer: "auto",
});
persistVisualiserLiveLayout(true);
expect(loadVisualiserDisplayPrefs().enablePhysics).toBe(true);
@@ -42,4 +46,12 @@ describe("settingsVisualiserPrefs", () => {
persistVisualiserAutoReload(true, { emit: false });
expect(loadVisualiserDisplayPrefs().autoReload).toBe(true);
});
+
+ it("normalizes and persists renderer preference", () => {
+ expect(normalizeVisualiserRenderer("nope")).toBe("auto");
+ persistVisualiserRenderer("webgl");
+ expect(loadVisualiserDisplayPrefs().renderer).toBe("webgl");
+ persistVisualiserRenderer("vis", { emit: false });
+ expect(loadVisualiserDisplayPrefs().renderer).toBe("vis");
+ });
});
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────